LangChain RAG 实践
目标:用 LangChain 在同一小文档库上跑通 ingest → 检索 → 生成,与 RAG 落地实践 的无框架 PoC 逐步对照。概念见 RAG 与检索增强。
本篇只覆盖 RAG 链路;Agent 图编排见 [LangGraph 编排实践](/docs/ai/Agent/LangGraph 编排实践)。
目标与验收
| 验收项 | 标准 |
|---|---|
| ingest | 2~3 个 .txt 经 Loader + Splitter 写入 Chroma |
| 检索 | retriever.invoke(query) 返回相关 Document |
| 生成 | Prompt 中可见引用片段;无命中时拒答 |
| 对照 | 能说出与无框架 PoC 各阶段的对应关系 |
建议目录
langchain_rag/
data/ # 与 mini_rag 相同的几篇短文
ingest.py
query.py
requirements.txt
requirements.txt 示例(版本以 LangChain 文档 为准):
langchain-core
langchain-community
langchain-text-splitters
langchain-chroma
chromadb
流水线映射
| RAG 落地实践 | LangChain 组件 |
|---|---|
chunk_text | RecursiveCharacterTextSplitter |
build_index 列表 | DocumentLoader + vector_store.add_documents |
score / retrieve | vector_store.as_retriever |
answer 模板 | Prompt 模板 + LLM(或模板拼接演示) |
切分与入库
# ingest.py
from pathlib import Path
from langchain_community.document_loaders import TextLoader
from langchain_community.embeddings import FakeEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
DATA_DIR = "data"
PERSIST_DIR = "chroma_db"
def ingest():
docs = []
for path in Path(DATA_DIR).glob("*.txt"):
docs.extend(TextLoader(str(path), encoding="utf-8").load())
splitter = RecursiveCharacterTextSplitter(
chunk_size=200, chunk_overlap=40, add_start_index=True
)
splits = splitter.split_documents(docs)
# PoC 用 FakeEmbeddings,无需 API;上线换真实 Embeddings 模型
embeddings = FakeEmbeddings(size=128)
vector_store = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory=PERSIST_DIR,
)
return vector_store
if __name__ == "__main__":
ingest()
print("indexed")
FakeEmbeddings 仅用于本地演示向量库流程;语义检索效果弱,换 OpenAIEmbeddings / 本地 HuggingFaceEmbeddings 后行为才接近生产。
检索
# query.py(片段)
def get_retriever(vector_store, k: int = 3):
return vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": k},
)
def retrieve_docs(retriever, query: str, score_threshold: float | None = None):
docs = retriever.invoke(query)
if not docs:
return []
# 可选:Chroma 带 score 的 similarity_search_with_score 做阈值拒答
return docs
加载已有索引:
from langchain_community.embeddings import FakeEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = FakeEmbeddings(size=128)
vector_store = Chroma(persist_directory=PERSIST_DIR, embedding_function=embeddings)
retriever = get_retriever(vector_store, k=3)
生成(模板 + 拒答)
无 LLM API 时,与无框架版同样用模板拼接;有 API 时把 context 塞进 Prompt 调模型即可。
def answer(query: str, docs: list) -> str:
if not docs:
return "知识库中未找到依据,无法回答。"
context = "\n---\n".join(
f"[{d.metadata.get('source', '?')}] {d.page_content}" for d in docs
)
return (
f"依据如下片段作答:\n{context}\n\n"
f"(此处可替换为 ChatOpenAI / 其他 LLM)\n问题:{query}"
)
# 主流程
# hits = retrieve_docs(retriever, "退货几天内可以?")
# print(answer("退货几天内可以?", hits))
接真 LLM 时常见写法(示意,import 路径以当前文档为准):
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "仅根据下列片段回答;无依据则说找不到。\n\n{context}"),
("human", "{question}"),
])
# chain = prompt | llm | StrOutputParser()
调参直觉
| 改动 | 与无框架 PoC 相同现象 |
|---|---|
chunk_size 变大 | 单段更全,噪声与定位变差 |
chunk_overlap 变大 | 边界语义更连贯,冗余与成本上升 |
k 变大 | 更易召回,也更易掺无关段 |
用 RAG 落地实践 里的 eval_set.jsonl 跑同一批问题,对比两种实现的 must_include 命中率。
常见坑
- 空库仍生成:检索无 Document 时必须拒答,不要直接调 LLM。
- Embedding 不一致:ingest 与 query 必须用同一 embedding 模型(含维度)。
- 中文与词袋:即使用向量,小模型对专有名词仍可能弱——引出混合检索 / 重排(见概念篇 Advanced 表)。
- API 漂移:LangChain 包拆分频繁(
langchain-community等),以官方文档 import 为准。
实践要点
- 先跑通 RAG 落地实践 无框架版,再对照本篇组件,理解更快。
- Loader → Splitter → VectorStore → Retriever 四步即工业界 RAG 主干,Agent 只是在之上加规划循环。
- PoC 用
FakeEmbeddings+ 模板生成;评测通过后再换真实 embedding 与 LLM。 - 向量 RAG 搞不定的全局归纳见 [GraphRAG 概览与查询](/docs/ai/RAG/GraphRAG 概览与查询)。
- 生产向量库见 [Milvus 向量库实践](/docs/ai/RAG/Milvus 向量库实践)(从 Chroma 迁移时通常只换 VectorStore)。
- 面试挂钩:能画 LangChain RAG 四组件,并说明与 Naive 流水线的一一对应即可。